#!/bin/bash

#-------------------------------DOCUMENT------------------------------------
#---------------------------------------------------------------------------
# shell测试框架
#---------------------------------------------------------------------------
# 1. mocker <cmd>: 对函数进行打桩
#   eg. mocker ls
#   a. mocker_invoke <func>: 执行打桩的函数时调用其他函数
#     ！！！注意：每一个打桩条件中必须有mocker_invoke，并且在最左侧！！！
#     eg. mocker_invoke self: 回调函数本身
#     eg. mocker_invoke none: 不回调任何函数，不执行任何操作
#     eg. mocker_invoke ls_stub: 执行ls_stub函数，ls_stub函数可以正常获取到ls的入参
#   b. mocker_condition <condition>: 条件判断，当条件满足时才执行对应打桩
#     eg. mocker_condition '[[ $1 == 'a' ]]': 当第一个参数为'a'时才执行打桩
#     eg. mocker_condition '[[ $1 == 'a' ]] && [[ $2 == 'b' ]]': 当第一个参数为'a'且第二个参数为'b'时才执行打桩
#     ！！！注意：两侧的引号必须为单引号，否则会导致解析错误！！！
#   c. mocker_id <id>: 指定打桩的id，用于后续判断命令执行次输
#     eg. mocker_id a: 指定id为a
#     eg. mocker_verify_times a 2: 验证id为a的命令执行次数是否为2
#   d. mocker_retcode <retcode>: 指定返回值
#     ！！！该命令需搭配mocker_invoke none使用！！！
#     eg. mocker_retcode 1: 指定返回值为1
#   e. mocker_output <output>: 指定输出内容
#     ！！！该命令需搭配mocker_invoke none使用！！！
#     eg. mocker_output "hello world": 指定输出内容为"hello world"
#   f. mocker_build: 完成打桩，在所有打桩命令执行完后，执行mocker_build命令
#   eg. 下面展示一次完整的打桩实例
#     mocker_this $(mocker ls |
#         mocker_invoke none | mocker_id a | mocker_condition '[[ $1 == "a" ]]' | mocker_retcode 0 | mocker_output "hello world" |
#         mocker_invoke ls_stub | mocker_id b | mocker_condition '[[ $1 == "b" ]]' |
#         mocker_invoke self |
#         mocker_build)
#     该用例对ls函数进行打桩，若第一个参数为'a'，则返回0，输出'hello world'；若第一个参数为'b'，则调用ls_stub函数；否则，调用原ls函数。
# 2. mocker_verify_times <id> <times>: 验证id为<id>的命令执行次数是否为<times>
#   eg. mocker_verify_times a 2: 验证id为a的命令执行次数是否为2
# 3. expect_eq <expect> <actual>: 断言expect等于actual，失败不会退出脚本
#   eg. expect_eq 1 1: 断言1等于1
# 4. expect_ne <expect> <actual>: 断言expect不等于actual，失败不会退出脚本
#   eg. expect_ne 1 2: 断言1不等于2
# 5. assert_eq <expect> <actual>: 断言expect等于actual，失败会退出脚本
#   eg. assert_eq 1 1: 断言1等于1
# 6. assert_ne <expect> <actual>: 断言expect不等于actual，失败会退出脚本
#   eg. assert_ne 1 2: 断言1不等于2
# 7. unmocker_all: 取消所有打桩
#   eg. unmocker_all: 取消所有打桩
# 8. unmocker_this <cmd>: 取消指定打桩
#   eg. unmocker_this ls: 取消ls函数的打桩

declare -A MOCKER_FUNCTION_TIMES_MAP

MOCKER_CMD_LIST=()

declare -i ERR_COUNT=0

current_cmd=""

function mocker() {
    echo "mocker $1"
}

function mocker_create() {
    source /tmp/function.sh
}

function mocker_invoke() {
    while IFS= read -r line; do
        echo "$line"
    done
    echo "-v $1"
}

function mocker_condition() {
    local mock_str=""
    while IFS= read -r line; do
        mock_str+="$line\n"
    done
    mock_str=${mock_str:0:${#mock_str} - 2}
    mock_str+=" -d \"$(echo $1 | sed 's/\$\([0-9]\+\)/\\\$\1/g')\""
    echo -e "$mock_str"
}

function mocker_id() {
    local mock_str=""
    while IFS= read -r line; do
        mock_str+="$line\n"
    done
    mock_str=${mock_str:0:${#mock_str} - 2}
    mock_str+=" -i $1"
    echo -e "$mock_str"
}

function mocker_retcode() {
    local mock_str=""
    while IFS= read -r line; do
        mock_str+="$line\n"
    done
    mock_str=${mock_str:0:${#mock_str} - 2}
    mock_str+=" -r $1"
    echo -e "$mock_str"
}

function mocker_output() {
    local mock_str=""
    while IFS= read -r line; do
        mock_str+="$line\n"
    done
    mock_str=${mock_str:0:${#mock_str} - 2}
    mock_str+=" -o \"$1\""
    echo -e "$mock_str"
}

function mocker_parse() {
    local content=""
    local condition=""
    local id=""
    local ret="return 0;"
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -v)
                shift
                if [[ $1 == "self" ]]; then
                    content+="command $current_cmd \$*;"
                else
                    content="$1 \$*;"
                fi
                ;;
            -d)
                shift
                condition+=$1
                ;;
            -i)
                shift
                id=$1
                MOCKER_FUNCTION_TIMES_MAP[$1]=0
                ;;
            -r)
                shift
                ret="return $1;"
                ;;
            -o)
                shift
                id=$1
                content+="echo $1;"
                ;;
        esac
        shift
    done

    if [[ ! -z $id ]]; then
        content+="MOCKER_FUNCTION_TIMES_MAP[$id]=\$((MOCKER_FUNCTION_TIMES_MAP[$id] + 1));"
    fi
    if [[ -z $condition ]]; then
        echo -e "$content $ret"
    else
        echo -e "if $condition; then $content $ret fi;"
    fi
}

function mocker_build() {
    local func_content=""
    local cmd=""
    while IFS= read -r line; do
        if [[ $line =~ mocker* ]]; then
            cmd=$(echo $line | awk '{print $2}')
            current_cmd=$cmd
            continue
        fi
        func_content+=$(eval "mocker_parse $line")
    done
    func_content="function none() { echo 'none' >> /dev/null; }\n function $cmd() { $func_content }"
    rm -f /tmp/function.sh
    echo -e $func_content >> /tmp/function.sh
    echo $cmd /tmp/function.sh
}

function mocker_this() {
    local cmd=$1
    MOCKER_CMD_LIST+=($cmd)
    source $2
}

mocker_verify_times() {
    local id=$1
    local times=$2
    if [[ -z $id ]]; then
        echo "Error: mocker_verify_times need id" >&2
        return 1
    fi
    if [[ -z $times ]]; then
        echo "Error: mocker_verify_times need times" >&2
        return 1
    fi
    if [[ -z ${MOCKER_FUNCTION_TIMES_MAP[$id]} ]]; then
        echo "Error: mocker_verify_times id not found" >&2
        return 1
    fi
    if [[ ${MOCKER_FUNCTION_TIMES_MAP[$id]} -ne $times ]]; then
        echo -e "\033[31mFAILED\033[0m: Condition $id call times not match, expect $times, actual ${MOCKER_FUNCTION_TIMES_MAP[$id]}"
        ERR_COUNT=$((ERR_COUNT + 1))
        exit 1
    fi
    return 0
}

expect_eq() {
    local expect=$1
    local actual=$2
    if [[ $expect != $actual ]]; then
        echo -e "\033[31mFAILED\033[0m: expect $expect, actual $actual. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        return
    fi
}

expect_ne() {
    local expect=$1
    local actual=$2
    if [[ $expect == $actual ]]; then
        echo -e "\033[31mFAILED\033[0m: expect not $expect, actual $actual. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        return
    fi
}

expect_file_exist() {
    local file=$1
    if [[ ! -f $file ]]; then
        echo -e "\033[31mFAILED\033[0m: file $file not exist. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        return
    fi
}

expect_dir_exist() {
    local dir=$1
    if [[ ! -d $dir ]]; then
        echo -e "\033[31mFAILED\033[0m: dir $dir not exist. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        return
    fi
}

assert_file_exist() {
    local file=$1
    if [[ ! -f $file ]]; then
        echo -e "\033[31mFAILED\033[0m: file $file not exist. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        exit 1
    fi
}

assert_dir_exist() {
    local dir=$1
    if [[ ! -d $dir ]]; then
        echo -e "\033[31mFAILED\033[0m: dir $dir not exist. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        exit 1
    fi
}

assert_eq() {
    local expect=$1
    local actual=$2
    if [[ $expect != $actual ]]; then
        echo -e "\033[31mFAILED\033[0m: assert $expect, actual $actual. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        exit 1
    fi
}

assert_ne() {
    local expect=$1
    local actual=$2
    if [[ $expect == $actual ]]; then
        echo -e "\033[31mFAILED\033[0m: assert not $expect, actual $actual. stack: $(caller 0)"
        ERR_COUNT=$((ERR_COUNT + 1))
        exit 1
    fi
}

mocker_fail() {
    echo -e "\033[31mFAILED\033[0m: $1. stack: $(caller 0)"
    ERR_COUNT=$((ERR_COUNT + 1))
    exit 1
}

unmocker_all() {
    for cmd in ${MOCKER_CMD_LIST[@]}; do
        unset -f $cmd
    done

    MOCKER_CMD_LIST=()
}

unmocker_this() {
    local cmd=$1
    if [[ ! " ${MOCKER_CMD_LIST[@]} " =~ " $cmd " ]]; then
        echo "Error: $cmd not found in mocker_this list"
        return 1
    fi
    unset -f $cmd
    MOCKER_CMD_LIST=(${MOCKER_CMD_LIST[@]/$cmd/})
}

mocker_set_root_dir() {
    local root_dir=$1
    if [[ -z $root_dir ]]; then
        echo "Error: mocker_set_root_dir need root_dir" >&2
        return 1
    fi

    export MOCKER_ROOT_DIR=$root_dir
}

function replace_symble() {
    echo $1 | sed 's/\//\\\//g'
}

function replace_path() {
    local file=$1
    cat $file | sed "s/\s\+\// $(replace_symble $MOCKER_ROOT_DIR)\//g" > $file
}

function prepare() {
    if [[ ! -f /tmp/prepare.txt ]]; then
        return 0
    fi

    rm -rf $MOCKER_ROOT_DIR
    mkdir -p $MOCKER_ROOT_DIR

    while IFS=' ' read src target needed; do
        if [[ -f $src ]]; then
            mkdir -p $MOCKER_ROOT_DIR/$target
            cp $src $MOCKER_ROOT_DIR/$target$(basename $src)
            replace_path $MOCKER_ROOT_DIR/$target$(basename $src)
            if [[ $needed -eq 1 ]]; then
                source $MOCKER_ROOT_DIR/$target$(basename $src)
            fi
        fi
    done < /tmp/prepare.txt
}

include() {
    local src=$1
    local dst=$2

    echo "$src $dst 0" >> /tmp/prepare.txt
}

import() {
    local src=$1
    local dst=$2

    echo "$src $dst 1" >> /tmp/prepare.txt
}